-
Notifications
You must be signed in to change notification settings - Fork 29.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
esm: Ensure custom loader resolved "url" is properly validated #21352
Conversation
@@ -0,0 +1,8 @@ | |||
export async function resolve(specifier, parentModuleURL, defaultResolve) { | |||
if (parentModuleURL && specifier !== 'assert') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: braces around multi-line condition body
lib/internal/modules/esm/loader.js
Outdated
new URL(url); | ||
} catch (err) { | ||
throw new ERR_INVALID_URL(url); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmmm... I'm not sure what the benefit of the try/catch and additional throw here. If the url
is invalid, new URL(url)
will throw and accomplish the same thing, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well caught, yes that's simpler. Updated.
lib/internal/modules/esm/loader.js
Outdated
@@ -64,6 +65,11 @@ class Loader { | |||
if (format === 'builtin') | |||
return { url: `node:${url}`, format }; | |||
|
|||
if (this._resolve !== defaultResolve) { | |||
// throws ERR_INVALID_URL for resolve if not valid |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think here's one of the places we can use a more descriptive error message. Something like "invalid URL returned by loader" is preferred over the user having to research why an error is thrown.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
stack trace should be enough right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so in this case, because it's not clear what the call hierarchy is. For example, if it's coming from the URL
constructor, then it's pretty obvious. But no one would know what Loader.resolve
is without reading the source code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I've done some polishing on the exact error messages.
assert.fail(); | ||
}, (err) => { | ||
assert.strictEqual(err.code, 'ERR_INVALID_URL'); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assume we would want .then(common.mustCall())
here?
65f89b7
to
ea909c3
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few nits. LGTM functionality-wise.
@@ -0,0 +1,12 @@ | |||
// Flags: --experimental-modules --loader ./test/fixtures/es-module-loaders/loader-invalid-url.mjs | |||
/* eslint-disable node-core/required-modules */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we move the import common
to before assert
to get rid of this override?
@@ -0,0 +1,9 @@ | |||
export async function resolve(specifier, parentModuleURL, defaultResolve) { | |||
if (parentModuleURL && specifier !== 'assert') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we have to exclude ../common
as well?
830956a
to
f4e6acf
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if overloading the ERR_INVALID_RETURN_VALUE
would be best for all use cases here. And another test to check the error in case of a returned primitive would be nice as well.
test/common/index.mjs
Outdated
import common from './index.js'; | ||
|
||
const { mustCall } = common; | ||
export { mustCall }; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If ./index.js
is imported, the already existing functionality here should be removed as that would be duplicated.
I suggest to export all common functions and to remove everything else from this file. I personally would move that code change to a individual commit.
assert.fail(); | ||
}, (err) => { | ||
assert.strictEqual(err.code, 'ERR_INVALID_RETURN_PROPERTY_STRING'); | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to use common.expectsError
here instead of the second function. That way the error message itself could also be verified.
The code could look like e.g.
.then(
assert.fail,
common.expectsError({
code: 'ERR_INVALID_RETURN_PROPERTY_STRING',
message: '...'
})
).then(mustCall());
lib/internal/errors.js
Outdated
return `Expected ${input} to be returned from the "${name}"` + | ||
` function but got ${type}.`; | ||
return `Expected ${input} to be returned from the ` + | ||
`"${name}" function but got ${type}.`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: this is a unrelated change.
lib/internal/errors.js
Outdated
}, TypeError); | ||
E('ERR_INVALID_RETURN_PROPERTY_STRING', (input, name, prop, value) => { | ||
return `Expected a valid ${input} to be returned for the ${prop} from the ` + | ||
`"${name}" function but got ${value}.`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: If this error code should still be kept, I suggest to change it to a string by reordering the arguments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because this is quite a complex arguments case, I think it is important to have the labelled arguments and also the order is important to the message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I already thought so but all other errors work like that and it should still be relatively clear what it is about.
lib/internal/modules/esm/loader.js
Outdated
@@ -56,16 +58,32 @@ class Loader { | |||
await this._resolve(specifier, parentURL, defaultResolve); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be useful to validate that the return type is actually an object. Otherwise the following error messages would still be weird (e.g., in case 5
is returned the error message would be about the url
property).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, I didn't realise destructuring didn't throw for such cases actually.
lib/internal/modules/esm/loader.js
Outdated
throw new ERR_INVALID_ARG_TYPE('format', 'string', format); | ||
throw new ERR_INVALID_RETURN_PROPERTY_STRING( | ||
'string', 'loader resolve', 'format', format | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This error should likely be ERR_INVALID_RETURN_PROPERTY
just as the error above. Or I fail to see why this is handled differently than the url
error case.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first error checks that it is a string, the second error checks that it is the right type of string.
The messages are:
- Expected string to be returned for the url from the "loader resolve" function but got ${type}.
- Expected a valid url to be returned for the url from the "loader resolve" function but got ${value}.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fail to see the second case being related to this error. It is about format
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh, I missed this was specifically the format case, but the same argument applied - format
must be one of a specific set of string values, so the error message will say:
Expected a valid format to be returned for the format from the "loader resolve" function but got ${value}
instead of
Expected string to be returned for the format from the "loader resolve" function but got string.
doc/api/errors.md
Outdated
### ERR_INVALID_RETURN_PROPERTY_STRING | ||
|
||
Thrown in case a function option does not return an expected string property | ||
type. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not sure why strings got special handled while other types not. I recommend to change this to: ERR_INVALID_RETURN_PROPERTY_VALUE
.
The current description is definitely wrong as the usage is not about the returned type but about the returned value (at least when looking at the actual implementation).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed on the semantics, but the thing is ideally the current ERR_INVALID_RETURN_VALUE
error should then be ERR_INVALID_RETURN_TYPE
as that is what it is really checking. So I was trying to remain consistent with that. I think ERR_INVALID_RETURN_PROPERTY_VALUE
isn't suitable for this reason. type
in the description is referring to "types of strings" as in categories / enums.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that the ERR_INVALID_RETURN_VALUE
could be improved. However the return checks are not common and could be consolidated to always be a ERR_INVALID_RETURN_VALUE
, just with a more specific error message. IMHO differentiating the type error from the value error is not useful for users.
That all aside: I am definitely against having a error code that is specific for property strings
. So at least that has to be more generalized out of my perspective.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO differentiating the type error from the value error is not useful for users.
See my example response in the last comment for why I definitely feel this is a useful distinction in the error message.
In terms of naming, I've switched them around to use ERR_INVALID_RETURN_PROPERTY_VALUE
to be consistent with return value, then made the new one just ERR_INVALID_RETURN_PROPERTY
.
@@ -1204,6 +1204,17 @@ An invalid `options.protocol` was passed. | |||
Both `breakEvalOnSigint` and `eval` options were set in the REPL config, which | |||
is not supported. | |||
|
|||
<a id="ERR_INVALID_RETURN_PROPERTY"></a> | |||
### ERR_INVALID_RETURN_PROPERTY |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name here might be better as: ERR_INVALID_RETURN_PROPERTY_TYPE
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, the consistent name here would be ERR_INVALID_RETURN_PROPERTY_VALUE
actually :)
f4e6acf
to
3bece0a
Compare
3bece0a
to
233cf2a
Compare
lib/internal/errors.js
Outdated
}, TypeError); | ||
E('ERR_INVALID_RETURN_PROPERTY_STRING', (input, name, prop, value) => { | ||
return `Expected a valid ${input} to be returned for the ${prop} from the ` + | ||
`"${name}" function but got ${value}.`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I already thought so but all other errors work like that and it should still be relatively clear what it is about.
lib/internal/modules/esm/loader.js
Outdated
throw new ERR_INVALID_ARG_TYPE('format', 'string', format); | ||
throw new ERR_INVALID_RETURN_PROPERTY_STRING( | ||
'string', 'loader resolve', 'format', format | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fail to see the second case being related to this error. It is about format
?
restoreStdout, | ||
restoreStderr, | ||
isCPPSymbolsNotMapped | ||
} = common; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be sufficient to just export common
, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can only do this once we have named exports support for CommonJS modules.
doc/api/errors.md
Outdated
### ERR_INVALID_RETURN_PROPERTY_STRING | ||
|
||
Thrown in case a function option does not return an expected string property | ||
type. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that the ERR_INVALID_RETURN_VALUE
could be improved. However the return checks are not common and could be consolidated to always be a ERR_INVALID_RETURN_VALUE
, just with a more specific error message. IMHO differentiating the type error from the value error is not useful for users.
That all aside: I am definitely against having a error code that is specific for property strings
. So at least that has to be more generalized out of my perspective.
30f9ab0
to
babc084
Compare
babc084
to
d434368
Compare
@BridgeAR let me know if you have further feedback on the error cases, otherwise could you mark your review on the error adjustments here as approved? |
Will merge this Monday / Tuesday if there is no further feedback. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would like to get some further tests in to actually check for all the new error cases before this lands.
lib/internal/modules/esm/loader.js
Outdated
throw new ERR_INVALID_ARG_TYPE('format', 'string', format); | ||
throw new ERR_INVALID_RETURN_PROPERTY( | ||
'module format', 'loader resolve', 'format', format | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am still not certain why typeof format !== 'string'
is an ERR_INVALID_RETURN_PROPERTY
while typeof url !== 'string'
is an ERR_INVALID_RETURN_PROPERTY_VALUE
. The validation step seems the same so it is somewhat weird for me that it results in different errors.
9607303
to
a235f08
Compare
@BridgeAR thanks, I've corrected the inconsistency there and added a test. Would be good to get this in soon if you can confirm a final review. |
Failure on the one linux bot appears unrelated, but just to be safe: https://ci.nodejs.org/job/node-test-commit-linux/19847/ (am I doing it right @Trott ;-P ...) |
That link seems to be to a CI job I started, not one that you restarted. In this particular case, I'd go to the node-test-pull-request job and use "Resume Build". That will create a new node-test-pull-request job with all the green stuff preserved (no re-run of it) but anything else (yellow, red, grey) re-run. I just did that, and here's the resulting node-test-pull-request: |
PR-URL: #21352 Reviewed-By: Gus Caplan <me@gus.host> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
This lands cleanly on v10.x-staging but the test fails. Maybe it depends on a previous change that wasn't backported yet? See https://github.com/nodejs/node/issues?q=label%3Abackport-requested-v10.x+is%3Aclosed+sort%3Aupdated-desc for the list of PRs waiting for a backport.
|
This is actually broken on master: |
The previously landed commit was broken and it’s too late to force-push. Fixing up the test seems to work. Refs: nodejs#21352
PR-URL: #21352 Reviewed-By: Gus Caplan <me@gus.host> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
This ensures that any loader returning a non-valid URL from the resolve hook will throw an error in the main resolution.
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passes